from collections.abc import Callable
from typing import Optional, Tuple
from types import AsyncGeneratorType
from inspect import iscoroutinefunction
import asyncio
from asyncio import Semaphore

from aiohttp import ClientSession
import async_timeout

from .exceptions import InvalidRequestMethod
from .response import Response
from sai.utils import logger

class Request:

    name = 'request'  # for logger name

    # Default request config
    REQUEST_CONFIG = {
        'RETRIES': 3,
        'DELAY': 0,
        'RETRY_DELAY': 3,
        'TIMEOUT': 20
    }

    METHOD = ['GET', 'POST']

    def __init__(self,
                 url: str,
                 method: str = 'GET',
                 *,
                 encoding: Optional[str]=None,
                 headers: dict=None,
                 callback: Callable=None,
                 metadata: dict=None,
                 request_config: dict=None,
                 request_session: Optional[ClientSession]=None,
                 **aiohttp_kwargs,  # 其他下载器需要的参数也通过这里传递进去
                 ):
        '''
        :param url: request url
        :param method: GET or POST
        :param encoding: Html encoding
        :param headers: request header
        :param callback:
        :param metadata: metadata
        :param request_config:
        :param request_session: aiohttp.ClientSession
        :param aiohttp_kwargs:
        '''
        self.url = url
        self.method = method.upper()

        if self.method not in self.METHOD:
            raise InvalidRequestMethod(f"{self.method} method is not supported!")

        self.callback: Callable = callback
        self.encoding: Optional[str] = encoding
        self.headers = headers
        self.metadata = metadata

        if request_config is None:
            self.request_config = self.REQUEST_CONFIG
        else:
            self.request_config = request_config

        self.request_session: ClientSession = request_session
        self.close_request_session: bool = False
        self.ssl = aiohttp_kwargs.pop("ssl", False)
        self.aiohttp_kwargs = aiohttp_kwargs
        self.retry_times = self.request_config.get("RETRIES", 3)

    @property
    def current_request_session(self):
        if self.request_session is None:
            self.request_session = ClientSession()
            self.close_request_session = True
        return self.request_session

    async def fetch(self, delay=True):
        # Fixme: random delay
        if delay and self.request_config.get('DELAY', 0) > 0:
            print(f"************ Delay {self.request_config.get('DELAY')} seconds")
            await asyncio.sleep(self.request_config['DELAY'])

        timeout = self.request_config.get('TIMEOUT', 20)

        try:
            async with async_timeout.timeout(timeout):  # if timeout, raise asyncio.TimeoutError
                resp = await self._make_request()
            try:
                resp_data = await resp.text(encoding=self.encoding)
            except UnicodeDecodeError:
                resp_data = await resp.read()

            response = Response(
                url=str(resp.url),
                method=resp.method,
                request_url=self.url,
                encoding=resp.get_encoding(),
                html=resp_data,
                metadata=self.metadata,
                cookies=resp.cookies,
                headers=resp.headers,
                status=resp.status,
                json=resp.json,
                text=resp.text,
                read=resp.read,
            )

            if response.ok:
                return response
            else:
                return await self._retry(
                    error_msg=f"Request url failed with status {response.status}!"
                )
        except asyncio.TimeoutError:
            return await self._retry(error_msg="timeout")
        except Exception as e:
            return await self._retry(error_msg=e)
        finally:
            await self._close_request()

    async def fetch_callback(
        self, sem: Semaphore, cookies=None
    ) -> Tuple[AsyncGeneratorType, Response]:
        """
        Request the target url and then call the callback function
        :param sem: Semaphore
        :return: Tuple[AsyncGeneratorType, Response]
        """
        try:
            async with sem:
                response = await self.fetch()
        except Exception as e:
            response = None
            logger.error(f"<Error: {self.url} {e}>")

        if self.callback is not None:
            if iscoroutinefunction(self.callback):
                callback_result = await self.callback(response)
            else:
                callback_result = self.callback(response)
        else:
            callback_result = None
        return callback_result, response

    async def _make_request(self):
        """Make a request by using aiohttp"""
        logger.info(f"<{self.method}: {self.url}>")
        if self.method == "GET":
            request_func = self.current_request_session.get(
                self.url, headers=self.headers, ssl=self.ssl, **self.aiohttp_kwargs
            )
        else:
            request_func = self.current_request_session.post(
                self.url, headers=self.headers, ssl=self.ssl, **self.aiohttp_kwargs
            )
        resp = await request_func
        return resp

    async def _close_request(self):
        if self.close_request_session:
            await self.request_session.close()

    async def _retry(self, error_msg):
        """Manage request"""
        if self.retry_times > 0:
            # Sleep to give server a chance to process/cache prior request
            if self.request_config.get("RETRY_DELAY", 0) > 0:
                await asyncio.sleep(self.request_config["RETRY_DELAY"])

            retry_times = self.request_config.get("RETRIES", 3) - self.retry_times + 1
            logger.error(
                f"<Retry url: {self.url}>, Retry times: {retry_times}, Retry message: {error_msg}>"
            )
            self.retry_times -= 1
            return await self.fetch(delay=False)
        else:
            response = Response(
                url=self.url,
                method=self.method,
                request_url=self.url,
                metadata=self.metadata,
                cookies={},
                headers=None,
            )

            # TODO: record error request

            return response

    def __repr__(self):
        return f"<{self.method} {self.url}>"